﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Text.RegularExpressions;

namespace XML2Nebula
{
	enum XMLElement
	{
		NebulaList,
		Nebula,
		Number,
		Description,
	}

	enum XMLAttribute
	{
		RightAscension,
		Declination,
		Magnitude,
		SurfaceBrightness,
		Class,
		Type,
		Catalog,
	}

	enum NumberType
	{
		Unknown,
		NGC,
		IC,
		Melotte,
		Messier,
		UGC,
		OCL,
	}

	enum NebulaType
	{
		Asterism,
		BrightNebula, //Bright Nebula
		ClusterNebulosity, //Cluster with Nebulosity
		DarkNebula, //Dark Nebula
		GalaxyCluster, //Galaxy cluster
		Galaxy, //Galaxy
		GlobularCluster, //Globular Cluster
		GalaxyDiffuseNebula, //Diffuse Nebula in a Galaxy
		GalaxyGlobularCluster, //Globular Cluster in a Galaxy
		GalaxyClusterNebulosity, //Cluster with Nebulosity in a Galaxy
		LMCClusterNebulosity, //Cluster with Nebulosity in the LMC
		LMCDiffuseNebula, //Diffuse Nebula in the LMC
		LMCGlobularCluster, //Globular Cluster in the LMC
		LMCOpenCluster, //Open cluster in the LMC
		Nonexistent,
		OpenCluster, //Open Cluster
		PlanetaryNebula, //Planetary Nebula
		SMCClusterNebulosity, //Cluster with Nebulosity in the SMC
		SMCDiffuseNebula, //Diffuse Nebula in the SMC
		SMCGlobularCluster, //Globular Cluster in the SMC
		SMCOpenCluster, //Open cluster in the SMC
		SupernovaRemnant, //Supernova Remnant
		Quasar,
		Stars //# Stars
	}

	[Flags]
	enum BCHMType
	{
		NGC = 1, //Best of the NGC from SAC
		Caldwell = 2, //Caldwell catalog from Patrick Moore
		Herschel400 = 4, //Herschel 400 from Astronomical League
		Messier = 8, //Messier object
	}

	internal static class ReaderExtensions
	{
		const string NS = "urn:denebsoft-skymap-nebula";

		internal static XPathNodeIterator SelectChildren(this XPathNavigator reader, XMLElement name)
		{
			return reader.SelectChildren(name.ToString(), NS);
		}

		internal static void ForEachChildren(this XPathNavigator reader, XMLElement name, Action<XPathNavigator> body)
		{
			var childs = reader.SelectChildren(name.ToString(), NS);
			foreach (XPathNavigator child in childs)
				body(child);
		}

		internal static void ForEachChildren(this XPathNavigator reader, XMLElement name, Action<int> count, Action<XPathNavigator> body)
		{
			var childs = reader.SelectChildren(name.ToString(), NS);
			count(childs.Count);
			foreach (XPathNavigator child in childs)
				body(child);
		}

		internal static double GetAttributeDouble(this XPathNavigator reader, XMLAttribute name)
		{
			string value = reader.GetAttribute(name.ToString(), String.Empty);
			if (String.IsNullOrEmpty(value))
				throw new ArgumentException(name.ToString());
			return XmlConvert.ToDouble(value);
		}

		internal static string GetAttribute(this XPathNavigator reader, XMLAttribute name)
		{
			string value = reader.GetAttribute(name.ToString(), String.Empty);
			return String.IsNullOrEmpty(value) ? " " : value;
		}

		internal static NebulaType GetAttributeType(this XPathNavigator reader, XMLAttribute name)
		{
			string code = reader.GetAttribute(name.ToString(), String.Empty);
			if (!Regex.IsMatch(code, @"\dStars")) //# Stars
				return (NebulaType)Enum.Parse(typeof(NebulaType), code);
			else
				return NebulaType.Stars + int.Parse(code.Substring(0, 1));
		}

		internal static TEnum GetAttribute<TEnum>(this XPathNavigator reader, XMLAttribute name, TEnum defValue) where TEnum: struct
		{
			string code = reader.GetAttribute(name.ToString(), String.Empty);
			if (String.IsNullOrEmpty(code))
				return defValue;
			return (TEnum)Enum.Parse(typeof(TEnum), code.Replace(" ", ", "));
		}

		internal static string GetAttributeLanguage(this XPathNavigator reader)
		{
			return reader.XmlLang;
		}
	}

	internal static class WriterExtensions
	{
		static string[] TypeCode = new string[]
		{
			"ASTER", //Asterism
			"BRTNB", //Bright Nebula
			"CL+NB", //Cluster with Nebulosity
			"DRKNB", //Dark Nebula
			"GALCL", //Galaxy cluster
			"GALXY", //Galaxy
			"GLOCL", //Globular Cluster
			"GX+DN", //Diffuse Nebula in a Galaxy
			"GX+GC", //Globular Cluster in a Galaxy
			"G+C+N", //Cluster with Nebulosity in a Galaxy
			"LMCCN", //Cluster with Nebulosity in the LMC
			"LMCDN", //Diffuse Nebula in the LMC
			"LMCGC", //Globular Cluster in the LMC
			"LMCOC", //Open cluster in the LMC
			"NONEX", //Nonexistent
			"OPNCL", //Open Cluster
			"PLNNB", //Planetary Nebula
			"SMCCN", //Cluster with Nebulosity in the SMC
			"SMCDN", //Diffuse Nebula in the SMC
			"SMCGC", //Globular Cluster in the SMC
			"SMCOC", //Open cluster in the SMC
			"SNREM", //Supernova Remnant
			"QUASR", //Quasar
		};

		static char[] BCHMChar = new char[]
		{
			'B', //Best of the NGC from SAC
			'C', //Caldwell catalog from Patrick Moore
			'H', //Herschel 400 from Astronomical League
			'M', //Messier object
		};

		internal static void Write(this BinaryWriter writer, double? value)
		{
			if (!value.HasValue)
				throw new ArgumentNullException("value");
			writer.Write(value.Value);
		}

		internal static void Write(this BinaryWriter writer, string number, NumberType type)
		{
			switch (type)
			{
			case NumberType.NGC: //New General Catalog of Nebulae & Clusters of Stars
				number = String.Format("NGC {0,4}", number);
				break;
			case NumberType.IC: //1st and 2nd Index Catalogs to the NGC
				number = String.Format("IC  {0,4}", number);
				break;
			case NumberType.Melotte: //Melotte (open clusters)
				number = String.Format("Mel {0,3}", number);
				break;
			case NumberType.Messier: //Messier objects
				number = String.Format("M{0}", number);
				break;
			case NumberType.UGC: //Uppsala General Catalog (galaxies)
				number = String.Format("UGC {0,5}", number);
				break;
			case NumberType.OCL:
				number = String.Format("OCL {0,4}", number);
				break;
			}

			writer.Write(number);
		}

		internal static void Write(this BinaryWriter writer, NebulaType value)
		{
			if (value < NebulaType.Stars)
				writer.Write(TypeCode[(int)value]);
			else
				writer.Write(((int)(value - NebulaType.Stars)).ToString() + "STAR");
		}

		internal static void Write(this BinaryWriter writer, BCHMType value)
		{
			string code = String.Empty;
			var values = Enum.GetValues(typeof(BCHMType));
			for (int i = 0; i < values.Length; i++)
			{
				if ((value & (BCHMType)values.GetValue(i)) != 0)
					code += BCHMChar[i];
			}
			writer.Write(code);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			string ifn = args.Length == 0 || Path.GetExtension(args[0]) == ".dat" ? "nebula.xml" : args[0];
			string ofn = args.Length == 1 && Path.GetExtension(args[0]) == ".dat" ? args[0] : args.Length <= 1 ? "nebula.dat" : args[1];

			if (args.Length > 2 || Path.GetExtension(ifn) != ".xml" || Path.GetExtension(ofn) != ".dat" ||
				args.Length > 1 && Path.GetExtension(args[0]) == ".dat")
			{
				Console.WriteLine("usage: {0} [nebula.xml] [nebula.dat]", Path.GetFileNameWithoutExtension(System.Reflection.Assembly.GetExecutingAssembly().Location));
				return;
			}

			var doc = new XPathDocument(ifn);
			var reader = doc.CreateNavigator();
			using (var ofs = new FileStream(ofn, FileMode.Create))
			using (var writer = new BinaryWriter(ofs))
			{
				reader.MoveToFirstChild(); //NebulaList

				reader.ForEachChildren(XMLElement.Nebula, count => writer.Write(count), neb =>
				{
					var RA = neb.GetAttributeDouble(XMLAttribute.RightAscension); //Right Ascension
					var Dec = neb.GetAttributeDouble(XMLAttribute.Declination);
					var magnitude = neb.GetAttributeDouble(XMLAttribute.Magnitude);
					var surfaceBr = neb.GetAttributeDouble(XMLAttribute.SurfaceBrightness); //Surface brightness (Reference Catalog of Galaxies 3)
					string Class = neb.GetAttribute(XMLAttribute.Class);
					NebulaType Type = neb.GetAttributeType(XMLAttribute.Type);
					BCHMType BCHM = neb.GetAttribute(XMLAttribute.Catalog, (BCHMType)0);

					var nums = neb.SelectChildren(XMLElement.Number);
					string Name = String.Empty;
					NumberType NameType = NumberType.Unknown;
					string OtherName = String.Empty;
					NumberType OtherType = NumberType.Unknown;
					if (nums.MoveNext())
					{
						NameType = nums.Current.GetAttribute(XMLAttribute.Type, NumberType.Unknown);
						Name = nums.Current.Value;
						if (nums.MoveNext())
						{
							OtherType = nums.Current.GetAttribute(XMLAttribute.Type, NumberType.Unknown);
							OtherName = nums.Current.Value;
						}
					}

					string Description = null;
					string Notes = null;
					string NotesRus = null;
					neb.ForEachChildren(XMLElement.Description, note =>
					{
						if (Description == null && note.GetAttribute(XMLAttribute.Type, NumberType.Unknown) == NumberType.NGC)
							Description = note.Value;
						else if (NotesRus == null && note.GetAttributeLanguage() == "ru")
							NotesRus = note.Value;
						else if (Notes == null)
							Notes = note.Value;
					});

					writer.Write(Name, NameType);
					writer.Write(OtherName, OtherType);
					writer.Write(RA);
					writer.Write(Dec);
					writer.Write(magnitude);
					writer.Write(surfaceBr);
					writer.Write(Class);
					writer.Write(Type);
					writer.Write(BCHM);
					writer.Write(Description ?? " ");
					writer.Write(Notes ?? " ");
				});
			}
		}
	}
}
